diff options
| author | Mohamed Bassem <me@mbassem.com> | 2024-11-23 20:59:34 +0000 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2024-11-23 20:59:34 +0000 |
| commit | 5522e20104da6afe2e4667cf45dbbbbc0e838865 (patch) | |
| tree | 72f416fa83c97a8533eea431e25bd63bda1e7d81 /apps/mobile/app/dashboard/bookmarks/[slug] | |
| parent | 4bb74872fd518008afea16a136292037baf5b024 (diff) | |
| download | karakeep-5522e20104da6afe2e4667cf45dbbbbc0e838865.tar.zst | |
ui(mobile): Replace bottom sheet with native screens (#690)
* Remove bottom sheet from bookmark info page
* Remove bottom sheet from manage lists page
* Remove bottom sheet from new list page
* Remove bottom sheet from new bookmark page
* Drop bottom-sheets
* Improve the look of the modals
* Make the search page fade from bottom
Diffstat (limited to 'apps/mobile/app/dashboard/bookmarks/[slug]')
| -rw-r--r-- | apps/mobile/app/dashboard/bookmarks/[slug]/index.tsx | 296 | ||||
| -rw-r--r-- | apps/mobile/app/dashboard/bookmarks/[slug]/info.tsx | 115 | ||||
| -rw-r--r-- | apps/mobile/app/dashboard/bookmarks/[slug]/manage_lists.tsx | 105 |
3 files changed, 516 insertions, 0 deletions
diff --git a/apps/mobile/app/dashboard/bookmarks/[slug]/index.tsx b/apps/mobile/app/dashboard/bookmarks/[slug]/index.tsx new file mode 100644 index 00000000..87330a88 --- /dev/null +++ b/apps/mobile/app/dashboard/bookmarks/[slug]/index.tsx @@ -0,0 +1,296 @@ +import React, { useState } from "react"; +import { + Alert, + Keyboard, + Linking, + Pressable, + ScrollView, + View, +} from "react-native"; +import ImageView from "react-native-image-viewing"; +import WebView from "react-native-webview"; +import { Stack, useLocalSearchParams, useRouter } from "expo-router"; +import BookmarkAssetImage from "@/components/bookmarks/BookmarkAssetImage"; +import BookmarkTextMarkdown from "@/components/bookmarks/BookmarkTextMarkdown"; +import FullPageError from "@/components/FullPageError"; +import { TailwindResolver } from "@/components/TailwindResolver"; +import { Button } from "@/components/ui/Button"; +import CustomSafeAreaView from "@/components/ui/CustomSafeAreaView"; +import FullPageSpinner from "@/components/ui/FullPageSpinner"; +import { Input } from "@/components/ui/Input"; +import { useToast } from "@/components/ui/Toast"; +import { useAssetUrl } from "@/lib/hooks"; +import { api } from "@/lib/trpc"; +import { ClipboardList, Globe, Info, Trash2 } from "lucide-react-native"; + +import { + useDeleteBookmark, + useUpdateBookmarkText, +} from "@hoarder/shared-react/hooks/bookmarks"; +import { BookmarkTypes, ZBookmark } from "@hoarder/shared/types/bookmarks"; + +function BottomActions({ bookmark }: { bookmark: ZBookmark }) { + const { toast } = useToast(); + const router = useRouter(); + const { mutate: deleteBookmark, isPending: isDeletionPending } = + useDeleteBookmark({ + onSuccess: () => { + router.back(); + toast({ + message: "The bookmark has been deleted!", + showProgress: false, + }); + }, + onError: () => { + toast({ + message: "Something went wrong", + variant: "destructive", + showProgress: false, + }); + }, + }); + + const deleteBookmarkAlert = () => + Alert.alert( + "Delete bookmark?", + "Are you sure you want to delete this bookmark?", + [ + { text: "Cancel", style: "cancel" }, + { + text: "Delete", + onPress: () => deleteBookmark({ bookmarkId: bookmark.id }), + style: "destructive", + }, + ], + ); + + const actions = [ + { + id: "lists", + icon: ( + <TailwindResolver + className="text-foreground" + comp={(styles) => <ClipboardList color={styles?.color?.toString()} />} + /> + ), + shouldRender: true, + onClick: () => + router.push(`/dashboard/bookmarks/${bookmark.id}/manage_lists`), + disabled: false, + }, + { + id: "open", + icon: ( + <TailwindResolver + className="text-foreground" + comp={(styles) => <Info color={styles?.color?.toString()} />} + /> + ), + shouldRender: true, + onClick: () => router.push(`/dashboard/bookmarks/${bookmark.id}/info`), + disabled: false, + }, + { + id: "delete", + icon: ( + <TailwindResolver + className="text-foreground" + comp={(styles) => <Trash2 color={styles?.color?.toString()} />} + /> + ), + shouldRender: true, + onClick: deleteBookmarkAlert, + disabled: isDeletionPending, + }, + { + id: "browser", + icon: ( + <TailwindResolver + className="text-foreground" + comp={(styles) => <Globe color={styles?.color?.toString()} />} + /> + ), + shouldRender: bookmark.content.type == BookmarkTypes.LINK, + onClick: () => + bookmark.content.type == BookmarkTypes.LINK && + Linking.openURL(bookmark.content.url), + disabled: false, + }, + ]; + return ( + <View> + <View className="flex flex-row items-center justify-between px-10 pb-2 pt-4"> + {actions.map( + (a) => + a.shouldRender && ( + <Pressable + disabled={a.disabled} + key={a.id} + onPress={a.onClick} + className="py-auto" + > + {a.icon} + </Pressable> + ), + )} + </View> + </View> + ); +} + +function BookmarkLinkView({ bookmark }: { bookmark: ZBookmark }) { + if (bookmark.content.type !== BookmarkTypes.LINK) { + throw new Error("Wrong content type rendered"); + } + return ( + <WebView + startInLoadingState={true} + mediaPlaybackRequiresUserAction={true} + source={{ uri: bookmark.content.url }} + /> + ); +} + +function BookmarkTextView({ bookmark }: { bookmark: ZBookmark }) { + if (bookmark.content.type !== BookmarkTypes.TEXT) { + throw new Error("Wrong content type rendered"); + } + const { toast } = useToast(); + + const [isEditing, setIsEditing] = useState(false); + const initialText = bookmark.content.text; + const [content, setContent] = useState(initialText); + + const { mutate, isPending } = useUpdateBookmarkText({ + onError: () => { + toast({ + message: "Something went wrong", + variant: "destructive", + }); + }, + onSuccess: () => { + setIsEditing(false); + }, + }); + + return ( + <View className="flex-1"> + {isEditing && ( + <View className="absolute right-0 top-0 z-10 m-4 flex flex-row gap-1"> + <Button label="Save" variant="default" onPress={Keyboard.dismiss} /> + <Button + label="Discard" + variant="destructive" + onPress={() => { + setContent(initialText); + setIsEditing(false); + }} + /> + </View> + )} + <ScrollView className="flex bg-background p-2"> + {isEditing ? ( + <Input + loading={isPending} + editable={!isPending} + onBlur={() => + mutate({ + bookmarkId: bookmark.id, + text: content, + }) + } + value={content} + onChangeText={setContent} + multiline + autoFocus + /> + ) : ( + <Pressable onPress={() => setIsEditing(true)}> + <View className="mb-4 rounded-xl border border-accent p-2"> + <BookmarkTextMarkdown text={content} /> + </View> + </Pressable> + )} + </ScrollView> + </View> + ); +} + +function BookmarkAssetView({ bookmark }: { bookmark: ZBookmark }) { + const [imageZoom, setImageZoom] = useState(false); + if (bookmark.content.type !== BookmarkTypes.ASSET) { + throw new Error("Wrong content type rendered"); + } + const assetSource = useAssetUrl(bookmark.content.assetId); + return ( + <View className="flex flex-1 gap-2"> + <ImageView + visible={imageZoom} + imageIndex={0} + onRequestClose={() => setImageZoom(false)} + doubleTapToZoomEnabled={true} + images={[assetSource]} + /> + + <Pressable onPress={() => setImageZoom(true)}> + <BookmarkAssetImage + assetId={bookmark.content.assetId} + className="h-56 min-h-56 w-full object-cover" + /> + </Pressable> + </View> + ); +} + +export default function ListView() { + const { slug } = useLocalSearchParams(); + if (typeof slug !== "string") { + throw new Error("Unexpected param type"); + } + + const { + data: bookmark, + error, + refetch, + } = api.bookmarks.getBookmark.useQuery({ bookmarkId: slug }); + + if (error) { + return <FullPageError error={error.message} onRetry={refetch} />; + } + + if (!bookmark) { + return <FullPageSpinner />; + } + + let comp; + let title = null; + switch (bookmark.content.type) { + case BookmarkTypes.LINK: + title = bookmark.title ?? bookmark.content.title; + comp = <BookmarkLinkView bookmark={bookmark} />; + break; + case BookmarkTypes.TEXT: + title = bookmark.title; + comp = <BookmarkTextView bookmark={bookmark} />; + break; + case BookmarkTypes.ASSET: + title = bookmark.title ?? bookmark.content.fileName; + comp = <BookmarkAssetView bookmark={bookmark} />; + break; + } + return ( + <CustomSafeAreaView edges={["bottom"]}> + <Stack.Screen + options={{ + headerTitle: title ?? "", + headerBackTitle: "Back", + headerTransparent: false, + }} + /> + <View className="flex h-full"> + {comp} + <BottomActions bookmark={bookmark} /> + </View> + </CustomSafeAreaView> + ); +} diff --git a/apps/mobile/app/dashboard/bookmarks/[slug]/info.tsx b/apps/mobile/app/dashboard/bookmarks/[slug]/info.tsx new file mode 100644 index 00000000..5d15ab6b --- /dev/null +++ b/apps/mobile/app/dashboard/bookmarks/[slug]/info.tsx @@ -0,0 +1,115 @@ +import React from "react"; +import { Text, View } from "react-native"; +import { Stack, useLocalSearchParams } from "expo-router"; +import TagPill from "@/components/bookmarks/TagPill"; +import FullPageError from "@/components/FullPageError"; +import CustomSafeAreaView from "@/components/ui/CustomSafeAreaView"; +import FullPageSpinner from "@/components/ui/FullPageSpinner"; +import { Input } from "@/components/ui/Input"; +import { Skeleton } from "@/components/ui/Skeleton"; +import { api } from "@/lib/trpc"; + +import { useUpdateBookmark } from "@hoarder/shared-react/hooks/bookmarks"; +import { isBookmarkStillTagging } from "@hoarder/shared-react/utils/bookmarkUtils"; +import { BookmarkTypes, ZBookmark } from "@hoarder/shared/types/bookmarks"; + +function TagList({ bookmark }: { bookmark: ZBookmark }) { + return ( + <View className="flex flex-row items-center gap-4"> + <Text className="text-foreground">Tags</Text> + {isBookmarkStillTagging(bookmark) ? ( + <> + <Skeleton className="h-4 w-full" /> + <Skeleton className="h-4 w-full" /> + </> + ) : bookmark.tags.length > 0 ? ( + <View className="flex flex-row flex-wrap gap-2"> + {bookmark.tags.map((t) => ( + <TagPill key={t.id} tag={t} /> + ))} + </View> + ) : ( + <Text className="text-foreground">No tags</Text> + )} + </View> + ); +} + +function NotesEditor({ bookmark }: { bookmark: ZBookmark }) { + const { mutate, isPending } = useUpdateBookmark(); + return ( + <View className="flex flex-row items-center gap-4"> + <Text className="text-foreground">Notes</Text> + + <Input + className="flex-1" + editable={!isPending} + multiline={true} + numberOfLines={3} + loading={isPending} + placeholder="Notes" + textAlignVertical="top" + onEndEditing={(ev) => + mutate({ + bookmarkId: bookmark.id, + note: ev.nativeEvent.text, + }) + } + defaultValue={bookmark.note ?? ""} + /> + </View> + ); +} + +const ViewBookmarkPage = () => { + const { slug } = useLocalSearchParams(); + if (typeof slug !== "string") { + throw new Error("Unexpected param type"); + } + const { + data: bookmark, + isPending, + refetch, + } = api.bookmarks.getBookmark.useQuery({ bookmarkId: slug }); + + if (isPending) { + return <FullPageSpinner />; + } + + if (!bookmark) { + return ( + <FullPageError error="Bookmark not found" onRetry={() => refetch()} /> + ); + } + + let title = null; + switch (bookmark.content.type) { + case BookmarkTypes.LINK: + title = bookmark.title ?? bookmark.content.title; + break; + case BookmarkTypes.TEXT: + title = bookmark.title; + break; + case BookmarkTypes.ASSET: + title = bookmark.title ?? bookmark.content.fileName; + break; + } + return ( + <CustomSafeAreaView> + <Stack.Screen + options={{ + headerShown: true, + headerTitle: title ?? "Untitled", + }} + /> + <View className="w-full p-4"> + <View className="gap-4 px-4"> + <TagList bookmark={bookmark} /> + <NotesEditor bookmark={bookmark} /> + </View> + </View> + </CustomSafeAreaView> + ); +}; + +export default ViewBookmarkPage; diff --git a/apps/mobile/app/dashboard/bookmarks/[slug]/manage_lists.tsx b/apps/mobile/app/dashboard/bookmarks/[slug]/manage_lists.tsx new file mode 100644 index 00000000..b38261df --- /dev/null +++ b/apps/mobile/app/dashboard/bookmarks/[slug]/manage_lists.tsx @@ -0,0 +1,105 @@ +import React from "react"; +import { FlatList, Pressable, Text, View } from "react-native"; +import Checkbox from "expo-checkbox"; +import { useLocalSearchParams } from "expo-router"; +import CustomSafeAreaView from "@/components/ui/CustomSafeAreaView"; +import { useToast } from "@/components/ui/Toast"; + +import { + useAddBookmarkToList, + useBookmarkLists, + useRemoveBookmarkFromList, +} from "@hoarder/shared-react/hooks/lists"; +import { api } from "@hoarder/shared-react/trpc"; + +const ListPickerPage = () => { + const { slug: bookmarkId } = useLocalSearchParams(); + if (typeof bookmarkId !== "string") { + throw new Error("Unexpected param type"); + } + const { toast } = useToast(); + const onError = () => { + toast({ + message: "Something went wrong", + variant: "destructive", + showProgress: false, + }); + }; + const { data: existingLists } = api.lists.getListsOfBookmark.useQuery( + { + bookmarkId, + }, + { + select: (data) => new Set(data.lists.map((l) => l.id)), + }, + ); + const { data } = useBookmarkLists(); + + const { mutate: addToList } = useAddBookmarkToList({ + onSuccess: () => { + toast({ + message: `The bookmark has been added to the list!`, + showProgress: false, + }); + }, + onError, + }); + + const { mutate: removeToList } = useRemoveBookmarkFromList({ + onSuccess: () => { + toast({ + message: `The bookmark has been removed from the list!`, + showProgress: false, + }); + }, + onError, + }); + + const toggleList = (listId: string) => { + if (!existingLists) { + return; + } + if (existingLists.has(listId)) { + removeToList({ bookmarkId, listId }); + } else { + addToList({ bookmarkId, listId }); + } + }; + + const { allPaths } = data ?? {}; + return ( + <CustomSafeAreaView> + <FlatList + className="h-full" + contentContainerStyle={{ + gap: 5, + }} + renderItem={(l) => ( + <View className="mx-2 flex flex-row items-center rounded-xl border border-input bg-white px-4 py-2 dark:bg-accent"> + <Pressable + key={l.item[l.item.length - 1].id} + onPress={() => toggleList(l.item[l.item.length - 1].id)} + className="flex w-full flex-row justify-between" + > + <Text className="text-lg text-accent-foreground"> + {l.item.map((item) => `${item.icon} ${item.name}`).join(" / ")} + </Text> + <Checkbox + value={ + existingLists && + existingLists.has(l.item[l.item.length - 1].id) + } + onValueChange={() => { + toggleList(l.item[l.item.length - 1].id); + }} + /> + </Pressable> + </View> + )} + data={allPaths} + /> + </CustomSafeAreaView> + ); +}; + +export default ListPickerPage; |
